use apikit_core::types::Method;
use async_trait::async_trait;
use futures::StreamExt;
use reqwest::Url;
use std::{convert::Infallible, error::Error, marker::PhantomData, str::FromStr};
use thiserror::Error;

use crate::param::ParamAppender;

#[async_trait(?Send)]
pub trait SseClient {
    type Error: Error;
    type ParamBuilder;
    type EventSource;

    async fn event_source<Param, Output>(
        &self,
        url: &str,
        method: Method,
        param: Param,
    ) -> Result<TypedEventSource<Self::EventSource, Output>, Self::Error>
    where
        Param: 'static,
        Param: ParamAppender<Self::ParamBuilder>,
        Self::EventSource: EventSource<Output>;
}

pub struct TypedEventSource<Inner, Output>
where
    Inner: EventSource<Output>,
{
    pub inner: Inner,
    _marker: PhantomData<Output>,
}

impl<Inner, Output> TypedEventSource<Inner, Output>
where
    Inner: EventSource<Output>,
{
    pub fn new(inner: Inner) -> Self {
        Self {
            inner,
            _marker: PhantomData,
        }
    }
}

#[derive(Debug, Error)]
pub enum ReqwestPollEventError {
    #[error(transparent)]
    RequestError(#[from] reqwest_eventsource::Error),

    #[error("{0}")]
    ConvertEventError(Box<dyn Error>),
}

#[async_trait(?Send)]
pub trait EventSource<Output> {
    type Error: Error;
    type Event;

    async fn next_event(&mut self) -> Option<Result<Output, Self::Error>>;
}

pub trait FromEvent<Event>: Sized {
    type Error: Error;

    fn from_event(event: Event) -> Result<Self, Self::Error>;
}

#[async_trait(?Send)]
impl<Inner, Output> EventSource<Output> for TypedEventSource<Inner, Output>
where
    Inner: EventSource<Output>,
    Output: FromEvent<Inner::Event>,
{
    type Error = Inner::Error;
    type Event = Inner::Event;

    async fn next_event(&mut self) -> Option<Result<Output, Self::Error>> {
        self.inner.next_event().await
    }
}

#[async_trait(?Send)]
impl<Output> EventSource<Output> for reqwest_eventsource::EventSource
where
    Output: FromEvent<reqwest_eventsource::Event> + 'static,
{
    type Error = ReqwestPollEventError;
    type Event = reqwest_eventsource::Event;

    async fn next_event(&mut self) -> Option<Result<Output, Self::Error>> {
        let event = self.next().await?;
        match event {
            Ok(event) => {
                let event = Output::from_event(event)
                    .map_err(|e| ReqwestPollEventError::ConvertEventError(Box::new(e)));
                Some(event)
            }
            Err(err) => Some(Err(err.into())),
        }
    }
}

#[derive(Debug, Error)]
pub enum ReqwestSseError {
    #[error("invalid method [{0}]")]
    InvalidMethod(String),

    #[error("invalid url [{0}]")]
    InvalidUrl(String),

    #[error("{0}")]
    AppendParamError(Box<dyn Error>),

    #[error(transparent)]
    ReqwestError(#[from] reqwest_eventsource::CannotCloneRequestError),
}

#[async_trait(?Send)]
impl SseClient for reqwest::Client {
    type Error = ReqwestSseError;
    type ParamBuilder = reqwest::RequestBuilder;
    type EventSource = reqwest_eventsource::EventSource;

    async fn event_source<Param, Output>(
        &self,
        url: &str,
        method: Method,
        param: Param,
    ) -> Result<TypedEventSource<Self::EventSource, Output>, Self::Error>
    where
        Param: 'static,
        Param: ParamAppender<Self::ParamBuilder>,
        reqwest_eventsource::EventSource: EventSource<Output>,
    {
        let builder: reqwest::RequestBuilder = self.request(
            method,
            Url::from_str(url).map_err(|_| ReqwestSseError::InvalidUrl(url.to_string()))?,
        );

        let builder = param
            .append_param(builder)
            .map_err(|e| ReqwestSseError::AppendParamError(Box::new(e)))?;
        let event_source = reqwest_eventsource::EventSource::new(builder)?;
        Ok(TypedEventSource::new(event_source))
    }
}

impl FromEvent<reqwest_eventsource::Event> for reqwest_eventsource::Event {
    type Error = Infallible;

    fn from_event(event: reqwest_eventsource::Event) -> Result<Self, Self::Error> {
        Ok(event)
    }
}
